//! Content identity entity

use crate::infra::sync::{ChangeType, SharedChangeEntry, Syncable};
use sea_orm::entity::prelude::*;
use sea_orm::{ActiveValue::NotSet, Set};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "content_identities")]
pub struct Model {
	#[sea_orm(primary_key)]
	pub id: i32,
	pub uuid: Option<Uuid>, // DETERMINISTIC from content_hash (assigned during content identification phase)
	pub integrity_hash: Option<String>, // Full hash for file validation (generated by validate job)
	pub content_hash: String, // Fast sampled hash for deduplication (generated during content identification)
	pub mime_type_id: Option<i32>,
	pub kind_id: i32, // ContentKind foreign key
	pub text_content: Option<String>,
	pub total_size: i64,  // Size of one instance of this content
	pub entry_count: i32, // Entries in THIS library only
	pub image_media_data_id: Option<i32>,
	pub video_media_data_id: Option<i32>,
	pub audio_media_data_id: Option<i32>,
	pub first_seen_at: DateTimeUtc,
	pub last_verified_at: DateTimeUtc,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
	#[sea_orm(has_many = "super::entry::Entity")]
	Entries,
	#[sea_orm(
		belongs_to = "super::content_kind::Entity",
		from = "Column::KindId",
		to = "super::content_kind::Column::Id"
	)]
	ContentKind,
	#[sea_orm(
		belongs_to = "super::mime_type::Entity",
		from = "Column::MimeTypeId",
		to = "super::mime_type::Column::Id"
	)]
	MimeType,
	#[sea_orm(
		belongs_to = "super::image_media_data::Entity",
		from = "Column::ImageMediaDataId",
		to = "super::image_media_data::Column::Id"
	)]
	ImageMediaData,
	#[sea_orm(
		belongs_to = "super::video_media_data::Entity",
		from = "Column::VideoMediaDataId",
		to = "super::video_media_data::Column::Id"
	)]
	VideoMediaData,
	#[sea_orm(
		belongs_to = "super::audio_media_data::Entity",
		from = "Column::AudioMediaDataId",
		to = "super::audio_media_data::Column::Id"
	)]
	AudioMediaData,
}

impl Related<super::entry::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Entries.def()
	}
}

impl Related<super::content_kind::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::ContentKind.def()
	}
}

impl Related<super::mime_type::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::MimeType.def()
	}
}

impl Related<super::image_media_data::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::ImageMediaData.def()
	}
}

impl Related<super::video_media_data::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::VideoMediaData.def()
	}
}

impl Related<super::audio_media_data::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::AudioMediaData.def()
	}
}

impl ActiveModelBehavior for ActiveModel {}

impl Model {
	/// Generate deterministic UUID from content_hash for sync consistency within library
	/// Note: ContentIdentity UUIDs are deterministic from content_hash + library_id
	/// This ensures same content in different libraries has different UUIDs
	/// Maintains library isolation while enabling deterministic sync
	pub fn deterministic_uuid(content_hash: &str, library_id: Uuid) -> Uuid {
		const LIBRARY_NAMESPACE: Uuid = Uuid::from_bytes([
			0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4,
			0x30, 0xc8,
		]);
		let namespace = Uuid::new_v5(&LIBRARY_NAMESPACE, library_id.as_bytes());
		Uuid::new_v5(&namespace, content_hash.as_bytes())
	}

	/// Calculate combined size on-demand (no need to cache entry_count * total_size)
	pub fn combined_size(&self) -> i64 {
		self.entry_count as i64 * self.total_size
	}
}

// Syncable Implementation
//
// ContentIdentity is a SHARED resource with deterministic UUIDs (derived from content_hash).
// Same content across devices has the same UUID, enabling automatic deduplication.
// Uses HLC-ordered log-based replication.
impl Syncable for Model {
	const SYNC_MODEL: &'static str = "content_identity";

	fn sync_id(&self) -> Uuid {
		self.uuid.expect("ContentIdentity must have UUID for sync")
	}

	fn version(&self) -> i64 {
		1
	}

	fn exclude_fields() -> Option<&'static [&'static str]> {
		Some(&[
			"id",
			"mime_type_id",
			"kind_id",
			"entry_count",
			"image_media_data_id",
			"video_media_data_id",
			"audio_media_data_id",
			"first_seen_at",
			"last_verified_at",
		])
	}

	fn sync_depends_on() -> &'static [&'static str] {
		&[]
	}

	// FK Lookup Methods (content_identity is FK target for entries)
	async fn lookup_id_by_uuid(
		uuid: Uuid,
		db: &DatabaseConnection,
	) -> Result<Option<i32>, sea_orm::DbErr> {
		use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
		Ok(Entity::find()
			.filter(Column::Uuid.eq(Some(uuid)))
			.one(db)
			.await?
			.map(|c| c.id))
	}

	async fn lookup_uuid_by_id(
		id: i32,
		db: &DatabaseConnection,
	) -> Result<Option<Uuid>, sea_orm::DbErr> {
		Ok(Entity::find_by_id(id).one(db).await?.and_then(|c| c.uuid))
	}

	async fn batch_lookup_ids_by_uuids(
		uuids: std::collections::HashSet<Uuid>,
		db: &DatabaseConnection,
	) -> Result<std::collections::HashMap<Uuid, i32>, sea_orm::DbErr> {
		use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
		if uuids.is_empty() {
			return Ok(std::collections::HashMap::new());
		}
		let uuid_opts: Vec<Option<Uuid>> = uuids.into_iter().map(Some).collect();
		let records = Entity::find()
			.filter(Column::Uuid.is_in(uuid_opts))
			.all(db)
			.await?;
		Ok(records
			.into_iter()
			.filter_map(|r| r.uuid.map(|u| (u, r.id)))
			.collect())
	}

	async fn batch_lookup_uuids_by_ids(
		ids: std::collections::HashSet<i32>,
		db: &DatabaseConnection,
	) -> Result<std::collections::HashMap<i32, Uuid>, sea_orm::DbErr> {
		use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
		if ids.is_empty() {
			return Ok(std::collections::HashMap::new());
		}
		let records = Entity::find().filter(Column::Id.is_in(ids)).all(db).await?;
		Ok(records
			.into_iter()
			.filter_map(|r| r.uuid.map(|u| (r.id, u)))
			.collect())
	}

	async fn query_for_sync(
		_device_id: Option<Uuid>,
		since: Option<chrono::DateTime<chrono::Utc>>,
		cursor: Option<(chrono::DateTime<chrono::Utc>, Uuid)>,
		batch_size: usize,
		db: &DatabaseConnection,
	) -> Result<Vec<(Uuid, serde_json::Value, chrono::DateTime<chrono::Utc>)>, sea_orm::DbErr> {
		use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter, QueryOrder, QuerySelect};

		let mut query = Entity::find().filter(Column::Uuid.is_not_null());

		if let Some(since_time) = since {
			query = query.filter(Column::LastVerifiedAt.gte(since_time));
		}

		// Cursor-based pagination with tie-breaker
		if let Some((cursor_ts, cursor_uuid)) = cursor {
			query = query.filter(
				Condition::any()
					.add(Column::LastVerifiedAt.gt(cursor_ts))
					.add(
						Condition::all()
							.add(Column::LastVerifiedAt.eq(cursor_ts))
							.add(Column::Uuid.gt(cursor_uuid)),
					),
			);
		}

		// Order by last_verified_at + uuid for deterministic pagination
		query = query
			.order_by_asc(Column::LastVerifiedAt)
			.order_by_asc(Column::Uuid);

		query = query.limit(batch_size as u64);

		let results = query.all(db).await?;

		let mut sync_results = Vec::new();
		for content in results {
			if content.uuid.is_none() {
				continue;
			}

			let json = match content.to_sync_json() {
				Ok(j) => j,
				Err(e) => {
					tracing::warn!(error = %e, content_hash = %content.content_hash, "Failed to serialize content_identity for sync");
					continue;
				}
			};

			sync_results.push((content.uuid.unwrap(), json, content.last_verified_at));
		}

		Ok(sync_results)
	}

	async fn apply_shared_change(
		entry: SharedChangeEntry,
		db: &DatabaseConnection,
	) -> Result<(), sea_orm::DbErr> {
		match entry.change_type {
			ChangeType::Insert | ChangeType::Update => {
				let data = entry.data.as_object().ok_or_else(|| {
					sea_orm::DbErr::Custom("ContentIdentity data is not an object".to_string())
				})?;

				let uuid: Uuid = serde_json::from_value(
					data.get("uuid")
						.ok_or_else(|| sea_orm::DbErr::Custom("Missing uuid".to_string()))?
						.clone(),
				)
				.map_err(|e| sea_orm::DbErr::Custom(format!("Invalid uuid: {}", e)))?;

				let content_hash: String = serde_json::from_value(
					data.get("content_hash")
						.ok_or_else(|| sea_orm::DbErr::Custom("Missing content_hash".to_string()))?
						.clone(),
				)
				.map_err(|e| sea_orm::DbErr::Custom(format!("Invalid content_hash: {}", e)))?;

				let active = ActiveModel {
					id: NotSet,
					uuid: Set(Some(uuid)),
					integrity_hash: Set(serde_json::from_value(
						data.get("integrity_hash")
							.cloned()
							.unwrap_or(serde_json::Value::Null),
					)
					.unwrap()),
					content_hash: Set(content_hash),
					mime_type_id: Set(None),
					kind_id: Set(1),
					text_content: Set(serde_json::from_value(
						data.get("text_content")
							.cloned()
							.unwrap_or(serde_json::Value::Null),
					)
					.unwrap()),
					total_size: Set(serde_json::from_value(
						data.get("total_size")
							.cloned()
							.unwrap_or(serde_json::Value::Null),
					)
					.unwrap()),
					entry_count: Set(0),
					image_media_data_id: Set(None),
					video_media_data_id: Set(None),
					audio_media_data_id: Set(None),
					first_seen_at: Set(chrono::Utc::now().into()),
					last_verified_at: Set(chrono::Utc::now().into()),
				};

				Entity::insert(active)
					.on_conflict(
						sea_orm::sea_query::OnConflict::column(Column::Uuid)
							.update_columns([
								Column::IntegrityHash,
								Column::ContentHash,
								Column::TextContent,
								Column::TotalSize,
								Column::LastVerifiedAt,
							])
							.to_owned(),
					)
					.exec(db)
					.await?;
			}

			ChangeType::Delete => {
				Entity::delete_many()
					.filter(Column::Uuid.eq(Some(entry.record_uuid)))
					.exec(db)
					.await?;
			}
		}

		Ok(())
	}
}

// Register with sync system via inventory
crate::register_syncable_shared!(Model, "content_identity", "content_identities");
